Описание данных:
Датасет содержит данные о событиях, впервые совершивших действия в мобильном приложении "Ненужные вещи". В нем пользователи продают свои ненужные вещи, размещая их на доске объявлений.
В датасете mobile_sources.csv содержатся:
В датасете mobile_dataset.csv содержатся:
Цели исследования: Получить на основе поведения пользователей гипотезы о том как можно было бы улучшить приложение с точки зрения пользовательского опыта. Разбить пользователей на отдельные сегменты по поведению. Выделить основные сценарии (последовательности событий) использования приложения.\ Заказчик продакт менеджер, который занимается вовлеченностью пользователей. В дальнейшем заказчик будет проводить свои исследования на основе нашей сегментации.
Ход исследования:
5.1. Проанализируйте связь целевого события — просмотра контактов — и других действий пользователей.
5.2. Оцените, какие действия чаще совершают те пользователи, которые просматривают контакты.
5.2.1 Рассчитать относительную частоту событий в разрезе двух групп пользователей:
С помощью данного исследования мы стремимся дать всестороннний анализ пользователей мобильного приложения "Ненужные вещи", что станет отправной точкой для дальнейшего развития компании.
# импортируем библиотеки pandas, matplot, numpy, scipy
import pandas as pd
import numpy as np
import seaborn as sns
import datetime as dt
import math as mth
from scipy import stats as st
from datetime import datetime, timedelta
from matplotlib import pyplot as plt
from plotly.subplots import make_subplots
import plotly.express as px
from plotly import graph_objects as go
import warnings
warnings.filterwarnings("ignore")
# устанавливаем отображение количества столбцов
pd.options.display.max_columns = 40
# устанавливаем отображение полного текста в ячейке
pd.set_option('display.max_colwidth', None)
# устанавливаем отображение чисел с двумя знаками после запятой
pd.set_option('display.float_format', '{:.2f}'.format)
# открываем файл
profiles = pd.read_csv('/Users/ildushisamov/Desktop/projects/final_project/mobile_app/mobile_sourсes.csv')
sessions = pd.read_csv('/Users/ildushisamov/Desktop/projects/final_project/mobile_app/mobile_dataset.csv')
# создадим универсальную функцию, которая будет принимать на вход датафрейм,
# а на выходе она будет выводить все нужные характеристики:
def my_func(x):
print('-'*15, 'Исходный датафрейм', '-'*15)
display(x.head())
print('')
print('')
print('-'*15, 'Общая информация о датафрейме', '-'*15)
print('')
print('')
x.info()
print('-'*15, 'Количество пустых значений в датафрейме', '-'*15)
print('')
print('')
display(x.isna().sum())
print('-'*15, 'Количество явных дубликатов в датафрейме', '-'*15)
display(x.duplicated().sum())
my_func(profiles)
--------------- Исходный датафрейм ---------------
| userId | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 |
--------------- Общая информация о датафрейме --------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 userId 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB --------------- Количество пустых значений в датафрейме ---------------
userId 0 source 0 dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
profiles.nunique()
userId 4293 source 3 dtype: int64
profiles['source'].unique()
array(['other', 'yandex', 'google'], dtype=object)
В датасете 4293 записей (вместе с шапкой таблицы) и 2 колонки:
Предобработка:
my_func(sessions)
--------------- Исходный датафрейм ---------------
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
--------------- Общая информация о датафрейме --------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event.time 74197 non-null object 1 event.name 74197 non-null object 2 user.id 74197 non-null object dtypes: object(3) memory usage: 1.7+ MB --------------- Количество пустых значений в датафрейме ---------------
event.time 0 event.name 0 user.id 0 dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
sessions.nunique()
event.time 74197 event.name 16 user.id 4293 dtype: int64
В датасете mobile_dataset 74197 записей (вместе с шапкой таблицы) и 3 колонки:
Предобработка:
sessions['event.name'].value_counts()
event.name tips_show 40055 photos_show 10012 advert_open 6164 contacts_show 4450 map 3881 search_1 3506 favorites_add 1417 search_5 1049 tips_click 814 search_4 701 contacts_call 541 search_3 522 search_6 460 search_2 324 search_7 222 show_contacts 79 Name: count, dtype: int64
Для удобства поиска сценариев стоит объединить действия пользователей:
# поменяем названия столбцов
profiles.rename(columns = {'userId': 'user_id'}, inplace = True)
sessions.columns = sessions.columns.str.replace('.', '_')
sessions.rename(columns = {'event_time': 'session_start'}, inplace = True)
display(list(profiles))
list(sessions)
['user_id', 'source']
['session_start', 'event_name', 'user_id']
# поменяем типы данных
sessions['session_start'] = pd.to_datetime(sessions['session_start'])
sessions.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 session_start 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 1.7+ MB
Добавим новый столбец день совершения определенного действия
sessions['dt'] = sessions['session_start'].dt.date
sessions
| session_start | event_name | user_id | dt | |
|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:53:29.534986 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 | 2019-11-03 |
| 74193 | 2019-11-03 23:54:00.407086 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 | 2019-11-03 |
| 74194 | 2019-11-03 23:56:57.041825 | search_1 | 20850c8f-4135-4059-b13b-198d3ac59902 | 2019-11-03 |
| 74195 | 2019-11-03 23:57:06.232189 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 | 2019-11-03 |
| 74196 | 2019-11-03 23:58:12.532487 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 | 2019-11-03 |
74197 rows × 4 columns
sessions['event_name'] = sessions['event_name'].str.replace('show_contacts', 'contacts_show')
sessions['event_name'] = sessions['event_name'].str.replace('^search_\d+', 'search', regex=True)
sessions['event_name'].value_counts()
event_name tips_show 40055 photos_show 10012 search 6784 advert_open 6164 contacts_show 4529 map 3881 favorites_add 1417 tips_click 814 contacts_call 541 Name: count, dtype: int64
В данном шаге мы проверили датасет на пропуски и дубликаты, привели данные в нужный тип, переименовали названия столбцов и объединили действия пользователей в группы.
После всех преобразований стоит проверить данные на наличие дубликатов. Этот шаг поможет обеспечить более точные результаты анализа.
my_func(profiles)
--------------- Исходный датафрейм ---------------
| user_id | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 |
--------------- Общая информация о датафрейме --------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB --------------- Количество пустых значений в датафрейме ---------------
user_id 0 source 0 dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
my_func(sessions)
--------------- Исходный датафрейм ---------------
| session_start | event_name | user_id | dt | |
|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
--------------- Общая информация о датафрейме --------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 session_start 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object 3 dt 74197 non-null object dtypes: datetime64[ns](1), object(3) memory usage: 2.3+ MB --------------- Количество пустых значений в датафрейме ---------------
session_start 0 event_name 0 user_id 0 dt 0 dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
Дубликатов не обнаружено.
Это функции для вычисления значений метрик:
get_profiles() — для создания профилей пользователей,get_retention() — для подсчёта Retention Rate.def get_profiles(sessions):
# сортируем сессии по ID пользователя и дате посещения
# группируем по ID и находим первые значения session_start и event_name
# столбец с временем первого посещения назовём first_ts
profiles = (
df.sort_values(by=['user_id', 'session_start'])
.groupby(['user_id', 'dt'])
.agg({'session_start': 'first', 'event_name': 'first'})
.rename(columns={'session_start': 'first_ts', 'event_name': 'first_event'})
.reset_index() # возвращаем user_id из индекса
)
# определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
# эти данные понадобятся для когортного анализа
profiles['dt'] = profiles['first_ts'].dt.date
return profiles
def get_retention(
profiles, sessions, observation_date, horizon_days, ignore_horizon=False
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
# вычисляем для каждого действия число недели, в котором было совершено действие
result_raw['dt_week'] = result_raw['first_ts'].dt.isocalendar().week
# вычисляем лайфтайм для каждой сессии в днях
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# создадим функцию для разделения дней лайфтаймов на недели
# 0-6 день - 1 неделя;
# 7-13 день - 2 неделя;
# 14-20 день - 3 неделя;
# 21-27 день - 4 неделя
def replace(lifetime):
if lifetime <= 6:
return 0
elif lifetime > 6 and lifetime <= 13:
return 1
elif lifetime > 13 and lifetime <= 20:
return 2
else:
return 3
result_raw['lifetime_week'] = result_raw['lifetime'].apply(replace)
# рассчитываем удержание
result_grouped = result_raw.pivot_table(
index=['dt_week'], columns='lifetime_week', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
result_raw.groupby('dt_week')
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result_grouped = cohort_sizes.merge(
result_grouped, on='dt_week', how='left'
).fillna(0)
result_grouped = result_grouped.div(result_grouped['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result_grouped = result_grouped[
['cohort_size'] + list(range(horizon_days))
]
# восстанавливаем столбец с размерами когорт
result_grouped['cohort_size'] = cohort_sizes
# возвращаем таблицу удержания и сырые данные
return result_raw, result_grouped
# соединяем 2 датасета
df = pd.merge(sessions, profiles, left_on='user_id', right_on='user_id')
df
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 2 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 3 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 4 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| ... | ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:46:47.068179 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74193 | 2019-11-03 23:46:58.914787 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74194 | 2019-11-03 23:47:01.232230 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74195 | 2019-11-03 23:47:47.475102 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74196 | 2019-11-03 23:47:50.087645 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 |
74197 rows × 5 columns
# создаем профиль пользователей
profiles = get_profiles(sessions)
profiles
| user_id | dt | first_ts | first_event | |
|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2019-10-09 18:33:55.577963 | map |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2019-10-21 19:52:30.778932 | tips_show |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 2019-10-22 11:18:14.635436 | map |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 2019-10-19 21:34:33.849769 | search |
| ... | ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | 2019-10-29 13:58:47.865084 | tips_show |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-30 | 2019-10-30 00:15:43.363752 | contacts_show |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-01 | 2019-11-01 00:24:31.162871 | tips_show |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-02 | 2019-11-02 01:16:48.947231 | tips_show |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show |
7817 rows × 4 columns
# посмотрим количество уникальных пользователей по первым действиям
(profiles
.groupby('first_event')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
| first_event | user_id | |
|---|---|---|
| 0 | tips_show | 1646 |
| 1 | search | 1343 |
| 2 | map | 908 |
| 3 | photos_show | 671 |
| 4 | contacts_show | 303 |
| 5 | advert_open | 204 |
| 6 | favorites_add | 71 |
| 7 | tips_click | 20 |
# построим график, отображающий динамику пользователей по каждому из первых действий
profiles.pivot_table(
index='dt', # даты первых посещений
columns='first_event', # первые действия
values='user_id', # ID пользователей
aggfunc='nunique' # подсчёт уникальных значений
).plot(figsize=(15, 5), grid=True)
plt.title('Диаграмма динамики пользователей по каждому из первых действий')
plt.xlabel('Дата')
plt.ylabel('Количество уникальных пользователей в день')
plt.show()
В течение всех 4 недель первые действия у пользователей были tips_show, т.е. первое и самое частое было то, что пользователи видели рекомендованное объявление, на втором месте находится search - пользователь искал интересное ему объявление.
# создаем профиль пользователей с последним действием
profiles_last = (
df.sort_values(by=['user_id', 'session_start'])
.groupby(['user_id', 'dt'])
.agg({'event_name': 'last'})
.rename(columns={'dt': 'last_dt', 'event_name': 'last_event'})
.reset_index()
)
profiles_last
| user_id | dt | last_event | |
|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | tips_show |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | tips_show |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | tips_show |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | photos_show |
| ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-30 | tips_show |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-01 | tips_show |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-02 | tips_show |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show |
7817 rows × 3 columns
# посмотрим количество уникальных пользователей по последним действиям
(profiles_last
.groupby('last_event')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
| last_event | user_id | |
|---|---|---|
| 0 | tips_show | 2558 |
| 1 | photos_show | 882 |
| 2 | search | 503 |
| 3 | contacts_show | 419 |
| 4 | map | 224 |
| 5 | advert_open | 211 |
| 6 | favorites_add | 134 |
| 7 | contacts_call | 133 |
| 8 | tips_click | 34 |
# построим график, отображающий динамику пользователей по каждому из последних действий
profiles_last.pivot_table(
index='dt', # даты первых посещений
columns='last_event', # источники переходов
values='user_id', # ID пользователей
aggfunc='nunique' # подсчёт уникальных значений
).plot(figsize=(15, 5), grid=True)
plt.title('Диаграмма динамики пользователей по каждому из последних действий')
plt.xlabel('Дата')
plt.ylabel('Количество уникальных пользователей в день')
plt.show()
Самым частым последним действием пользователей за 4 недели, было тоже самое действие - tips_show, на втором месте находится photos_show - пользователи просматривали фото в объявлении.
# присоединяем session_start и event_name к профилям по столбцу user_id и dt,
# чтобы добавить event_name к каждой сессии
result_raw = profiles.merge(
sessions[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])
result_raw['session_dt'] = result_raw['session_start'].dt.date
# вычисляем для каждого действия число недели, в котором было совершено действие
result_raw['dt_week'] = result_raw['first_ts'].dt.isocalendar().week
result_raw
| index | user_id | dt | first_ts | first_event | session_start | event_name | session_dt | dt_week | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 | 41 |
| 1 | 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:40:31.052909 | tips_show | 2019-10-07 | 41 |
| 2 | 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:41:05.722489 | tips_show | 2019-10-07 | 41 |
| 3 | 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:43:20.735461 | tips_show | 2019-10-07 | 41 |
| 4 | 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:45:30.917502 | tips_show | 2019-10-07 | 41 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 74192 | 74192 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 15:51:23.959572 | tips_show | 2019-11-03 | 44 |
| 74193 | 74193 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 15:51:57.899997 | contacts_show | 2019-11-03 | 44 |
| 74194 | 74194 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:07:40.932077 | tips_show | 2019-11-03 | 44 |
| 74195 | 74195 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:08:18.202734 | tips_show | 2019-11-03 | 44 |
| 74196 | 74196 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:08:25.388712 | tips_show | 2019-11-03 | 44 |
74197 rows × 9 columns
# посмотрим общую информацию по таблице
result_raw.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 index 74197 non-null int64 1 user_id 74197 non-null object 2 dt 74197 non-null object 3 first_ts 74197 non-null datetime64[ns] 4 first_event 74197 non-null object 5 session_start 74197 non-null datetime64[ns] 6 event_name 74197 non-null object 7 session_dt 74197 non-null object 8 dt_week 74197 non-null UInt32 dtypes: UInt32(1), datetime64[ns](2), int64(1), object(5) memory usage: 4.9+ MB
# посмотрим количество сессий по действиям
(result_raw
.groupby('event_name')
.agg({'session_start': 'count'})
.sort_values(by='session_start', ascending=False)
)
| session_start | |
|---|---|
| event_name | |
| tips_show | 40055 |
| photos_show | 10012 |
| search | 6784 |
| advert_open | 6164 |
| contacts_show | 4529 |
| map | 3881 |
| favorites_add | 1417 |
| tips_click | 814 |
| contacts_call | 541 |
Самым частым действием за 4 недели является также tips_show (40 тыс. действий) оно идёт в отрыве х4 от действия photos_show (10 тыс. действий), которое находится на 2 месте. Можно сказать, что люди которые пользуются приложением, только и видят рекомендованные объявления, которые отображаются автоматически и не зависят от их действий. Для некоторых расчетов в исследованиях это события учитывать не нужно, поэтому сделаем отдельный датасет с событиями без tips_show (event_name != tips_show).
Также важно отметить тот факт, что кликов по этим самым рекомендованным объявлениям всего - 814 совершенных действий. Подсчёт конверсий будем производить чуть позже.
result_raw['dt_week'].sort_values().unique()
<IntegerArray> [41, 42, 43, 44] Length: 4, dtype: UInt32
Получилось 4 недели.
print('Минимальная дата совершения действия:', df['dt'].unique().min())
print()
print('Максимальная дата совершения действия:', df['dt'].unique().max())
Минимальная дата совершения действия: 2019-10-07 Максимальная дата совершения действия: 2019-11-03
Данные компании выгружаются ежемесячно. Значит моментом анализа является - максимальная дата совершения действия, а горизонтом анализа оптимальней и удобней всего для анализа будет поставить промежуток времени - неделю. Удобней и для прогнозирования рекламных расходов в будущем, и для подсчетов Retention и Conversion Rate.
Также при горизонте анализа в неделю датасет разделится ровно на 4 недели.
# задаём момент и горизонт анализа данных
observation_date = datetime(2019, 11, 3).date()
horizon_weeks = 4
# создаём опцию «игнорировать горизонт»
ignore_horizon = False
retention_raw, retention = get_retention(
profiles, sessions, observation_date, horizon_weeks
)
retention
| cohort_size | 0 | 1 | 2 | 3 | |
|---|---|---|---|---|---|
| dt_week | |||||
| 41 | 1130 | 1.00 | 0.19 | 0.13 | 0.07 |
| 42 | 1438 | 1.00 | 0.21 | 0.10 | 0.00 |
| 43 | 1546 | 1.00 | 0.15 | 0.00 | 0.00 |
| 44 | 962 | 1.00 | 0.00 | 0.00 | 0.00 |
# строим хитмэп
plt.figure(figsize=(14, 10)) # задаём размер графика
sns.heatmap(
retention.drop(columns=['cohort_size']), # удаляем размеры когорт
annot=True, # включаем подписи
fmt='.2%', # переводим значения в проценты
cmap= 'coolwarm',
yticklabels= ['1', '2', '3', '4'],
xticklabels= ['1', '2', '3', '4']
)
plt.xlabel('Неделя лайфтаймов')
plt.ylabel('Неделя привлечения')
plt.title('Тепловая карта удержания') # название графика
plt.show()
RR второй недели лайфтайма пришедших в 1 и 2 неделю (19,12% и 20,93%) заметно выше, по сравнению с пришедшими в 3 неделю (15,33%).
RR третьей недели лайфтайма выглядит заметно лучше у когорты пришедшей в 1 неделю (12,65%), чем у когорты пришедшей во неделе (9,67%).
Хочу отметить, что данный рассматриваемый промежуток очень короткий, и нужно больше времени, чтобы сделать достоверные выводы. Но если делать промежуточный вывод с теми данными, которые мы сейчас имеем, то можно сказать, что когорта пришедшая в 1 неделю выглядит уверенней всех остальных, хотя RR второй недели был выше у когорты пришедшей во вторую неделю. Привлеченные пользователи показывают RR в первую неделю лайфтайма от 15 до 21% удерживания пользователей, в третью неделю от 9,5 до 12,5%, а в четвертую неделю - 7.17% удержания.
Таким образом, стоит рассмотреть каналы привлечения пользователей, рассчитать RR ещё раз уже с более длинным диапазоном времени и понять какие каналы привлечения более привлекательны в рамках расчетов удержания пользователей.
Для простоты расчета буду использовать календарный день за одну сессию.
Сессия = Календарный день
result_raw
| index | user_id | dt | first_ts | first_event | session_start | event_name | session_dt | dt_week | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 | 41 |
| 1 | 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:40:31.052909 | tips_show | 2019-10-07 | 41 |
| 2 | 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:41:05.722489 | tips_show | 2019-10-07 | 41 |
| 3 | 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:43:20.735461 | tips_show | 2019-10-07 | 41 |
| 4 | 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 13:45:30.917502 | tips_show | 2019-10-07 | 41 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 74192 | 74192 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 15:51:23.959572 | tips_show | 2019-11-03 | 44 |
| 74193 | 74193 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 15:51:57.899997 | contacts_show | 2019-11-03 | 44 |
| 74194 | 74194 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:07:40.932077 | tips_show | 2019-11-03 | 44 |
| 74195 | 74195 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:08:18.202734 | tips_show | 2019-11-03 | 44 |
| 74196 | 74196 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show | 2019-11-03 16:08:25.388712 | tips_show | 2019-11-03 | 44 |
74197 rows × 9 columns
result_raw_without_tips_show = result_raw.query('event_name != "tips_show"') result_raw_without_tips_show
profiles
| user_id | dt | first_ts | first_event | |
|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 2019-10-07 13:39:45.989359 | tips_show |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2019-10-09 18:33:55.577963 | map |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2019-10-21 19:52:30.778932 | tips_show |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 2019-10-22 11:18:14.635436 | map |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 2019-10-19 21:34:33.849769 | search |
| ... | ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | 2019-10-29 13:58:47.865084 | tips_show |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-30 | 2019-10-30 00:15:43.363752 | contacts_show |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-01 | 2019-11-01 00:24:31.162871 | tips_show |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-02 | 2019-11-02 01:16:48.947231 | tips_show |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 2019-11-03 14:32:55.956301 | tips_show |
7817 rows × 4 columns
# группируем датасет по session_dt, user_id, first_event
# тем самым получаем количество действий пользователя в день,
# а индексы будут означать идентификатор каждой сессии, их переименуем в session_id
final_df = result_raw.pivot_table(
index = ['session_dt', 'user_id', 'first_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})
final_df = final_df.reset_index().rename(columns = {'index': 'session_id'})
final_df
| session_id | session_dt | user_id | first_event | cnt_sessions | |
|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | tips_show | 9 |
| 1 | 1 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 |
| 2 | 2 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 28 |
| 3 | 3 | 2019-10-07 | 024828cf-c873-43e6-8c7e-96aeb348699e | tips_show | 2 |
| 4 | 4 | 2019-10-07 | 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 | search | 15 |
| ... | ... | ... | ... | ... | ... |
| 7812 | 7812 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | search | 9 |
| 7813 | 7813 | 2019-11-03 | f8cfaf96-f241-4da7-ae8a-618aaf3a7fee | tips_show | 6 |
| 7814 | 7814 | 2019-11-03 | f9b53876-74f9-45ea-b83d-b5722e0172af | tips_show | 4 |
| 7815 | 7815 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | search | 3 |
| 7816 | 7816 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | tips_show | 29 |
7817 rows × 5 columns
# далее присоединяем к прошлой таблице наш датасет, чтобы дальше можно было сгруппировать по event_name
# избавимся от повторяющихся событий в рамках сессии и оставляем последние действия
final_df = final_df.merge(
result_raw, on = ['user_id', 'session_dt', 'first_event'], how = 'left'
).drop_duplicates(subset = ['session_id', 'event_name'], keep='last').reset_index(drop=True)
final_df
| session_id | session_dt | user_id | first_event | cnt_sessions | index | dt | first_ts | session_start | event_name | dt_week | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | tips_show | 9 | 8 | 2019-10-07 | 2019-10-07 13:39:45.989359 | 2019-10-07 13:49:41.716617 | tips_show | 41 |
| 1 | 1 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 | 270 | 2019-10-07 | 2019-10-07 22:00:35.765785 | 2019-10-07 22:01:43.074554 | search | 41 |
| 2 | 2 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 28 | 521 | 2019-10-07 | 2019-10-07 00:00:00.431357 | 2019-10-07 00:15:47.030323 | map | 41 |
| 3 | 2 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 28 | 523 | 2019-10-07 | 2019-10-07 00:00:00.431357 | 2019-10-07 00:18:37.324815 | advert_open | 41 |
| 4 | 2 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 28 | 524 | 2019-10-07 | 2019-10-07 00:00:00.431357 | 2019-10-07 00:18:42.917148 | tips_show | 41 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 14769 | 7815 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | search | 3 | 72179 | 2019-11-03 | 2019-11-03 11:38:41.424157 | 2019-11-03 11:38:41.424157 | search | 44 |
| 14770 | 7815 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | search | 3 | 72180 | 2019-11-03 | 2019-11-03 11:38:41.424157 | 2019-11-03 11:39:32.218473 | contacts_show | 44 |
| 14771 | 7815 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | search | 3 | 72181 | 2019-11-03 | 2019-11-03 11:38:41.424157 | 2019-11-03 11:39:43.578085 | contacts_call | 44 |
| 14772 | 7816 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | tips_show | 29 | 74193 | 2019-11-03 | 2019-11-03 14:32:55.956301 | 2019-11-03 15:51:57.899997 | contacts_show | 44 |
| 14773 | 7816 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | tips_show | 29 | 74196 | 2019-11-03 | 2019-11-03 14:32:55.956301 | 2019-11-03 16:08:25.388712 | tips_show | 44 |
14774 rows × 11 columns
# выведем все сценарии для каждой сессии:
# сгруппируем по session_id и добавим вывод сценариев с функцией лямбда и добавлением действий поочердно в кортеж
scenarios = (final_df
.groupby('session_id')
.agg({'event_name':lambda x: tuple(x)})
.reset_index()
.rename(columns = {'event_name': 'scenarios'})
)
scenarios
| session_id | scenarios | |
|---|---|---|
| 0 | 0 | (tips_show,) |
| 1 | 1 | (search,) |
| 2 | 2 | (map, advert_open, tips_show) |
| 3 | 3 | (tips_show,) |
| 4 | 4 | (search, tips_show) |
| ... | ... | ... |
| 7812 | 7812 | (search, contacts_show, tips_show) |
| 7813 | 7813 | (tips_show, advert_open) |
| 7814 | 7814 | (tips_show,) |
| 7815 | 7815 | (search, contacts_show, contacts_call) |
| 7816 | 7816 | (contacts_show, tips_show) |
7817 rows × 2 columns
# посчитаем количество уникальных сценарий
scenarios['scenarios'].nunique()
412
Всего 412 уникальных сценарий.
Для некоторых задач требуется собрать датасет без событий tips_show, т.к. это действие не является показателем поведения пользователей.
Для решения этой задачи нужно оставить только те сценарии, в которых последнее действие - целевое событие (contacts_show).
df
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 2 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 3 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 4 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| ... | ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:46:47.068179 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74193 | 2019-11-03 23:46:58.914787 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74194 | 2019-11-03 23:47:01.232230 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74195 | 2019-11-03 23:47:47.475102 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74196 | 2019-11-03 23:47:50.087645 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 |
74197 rows × 5 columns
df_without_tips_show = df.query('event_name != "tips_show"') df_without_tips_show
# создаем профиль пользователей с последним действием
profiles_last = (
df.sort_values(by=['user_id', 'session_start'])
.groupby(['user_id', 'dt'])
.agg({'event_name': 'last'})
.rename(columns={'event_name': 'last_event', 'dt': 'last_dt'})
.reset_index()
)
profiles_last
| user_id | dt | last_event | |
|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | tips_show |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | tips_show |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | tips_show |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | photos_show |
| ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-30 | tips_show |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-01 | tips_show |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-02 | tips_show |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show |
7817 rows × 3 columns
# соединим таблицы, чтобы получить к каждому last_event - событие с event_name
result_raw_last = profiles_last.merge(
sessions[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])
result_raw_last['session_dt'] = result_raw_last['session_start'].dt.date
result_raw_last
| index | user_id | dt | last_event | session_start | event_name | session_dt | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 |
| 1 | 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:40:31.052909 | tips_show | 2019-10-07 |
| 2 | 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:41:05.722489 | tips_show | 2019-10-07 |
| 3 | 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:43:20.735461 | tips_show | 2019-10-07 |
| 4 | 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:45:30.917502 | tips_show | 2019-10-07 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 74192 | 74192 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 15:51:23.959572 | tips_show | 2019-11-03 |
| 74193 | 74193 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 15:51:57.899997 | contacts_show | 2019-11-03 |
| 74194 | 74194 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:07:40.932077 | tips_show | 2019-11-03 |
| 74195 | 74195 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:08:18.202734 | tips_show | 2019-11-03 |
| 74196 | 74196 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:08:25.388712 | tips_show | 2019-11-03 |
74197 rows × 7 columns
# группируем датасет по session_dt, user_id, last_event
# тем самым получаем количество действий пользователя в день
# а индексы будут означать session_id каждой сессии, их переименуем в session_id
final_df_last = result_raw_last.pivot_table(
index = ['session_dt', 'user_id', 'last_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})
final_df_last = final_df_last.reset_index().rename(columns = {'index': 'session_id'})
final_df_last
#final_df_last.query('user_id == "00157779-810c-4498-9e05-a1e9e3cedf93"')
| session_id | session_dt | user_id | last_event | cnt_sessions | |
|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | tips_show | 9 |
| 1 | 1 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 |
| 2 | 2 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | tips_show | 28 |
| 3 | 3 | 2019-10-07 | 024828cf-c873-43e6-8c7e-96aeb348699e | tips_show | 2 |
| 4 | 4 | 2019-10-07 | 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 | tips_show | 15 |
| ... | ... | ... | ... | ... | ... |
| 7812 | 7812 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | tips_show | 9 |
| 7813 | 7813 | 2019-11-03 | f8cfaf96-f241-4da7-ae8a-618aaf3a7fee | advert_open | 6 |
| 7814 | 7814 | 2019-11-03 | f9b53876-74f9-45ea-b83d-b5722e0172af | tips_show | 4 |
| 7815 | 7815 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 |
| 7816 | 7816 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | tips_show | 29 |
7817 rows × 5 columns
# добавим к нашей таблице колонку session_id
final_df = result_raw_last.merge(
final_df_last,
on=['session_dt', 'user_id', 'last_event'],
how='left')
# добавим к нашей таблице колонку scenarios
final_df = final_df.merge(scenarios, on='session_id', how='left')
final_df
| index | user_id | dt | last_event | session_start | event_name | session_dt | session_id | cnt_sessions | scenarios | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 | 0 | 9 | (tips_show,) |
| 1 | 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:40:31.052909 | tips_show | 2019-10-07 | 0 | 9 | (tips_show,) |
| 2 | 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:41:05.722489 | tips_show | 2019-10-07 | 0 | 9 | (tips_show,) |
| 3 | 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:43:20.735461 | tips_show | 2019-10-07 | 0 | 9 | (tips_show,) |
| 4 | 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | tips_show | 2019-10-07 13:45:30.917502 | tips_show | 2019-10-07 | 0 | 9 | (tips_show,) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 74192 | 74192 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 15:51:23.959572 | tips_show | 2019-11-03 | 7816 | 29 | (contacts_show, tips_show) |
| 74193 | 74193 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 15:51:57.899997 | contacts_show | 2019-11-03 | 7816 | 29 | (contacts_show, tips_show) |
| 74194 | 74194 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:07:40.932077 | tips_show | 2019-11-03 | 7816 | 29 | (contacts_show, tips_show) |
| 74195 | 74195 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:08:18.202734 | tips_show | 2019-11-03 | 7816 | 29 | (contacts_show, tips_show) |
| 74196 | 74196 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | tips_show | 2019-11-03 16:08:25.388712 | tips_show | 2019-11-03 | 7816 | 29 | (contacts_show, tips_show) |
74197 rows × 10 columns
# оставим в датасете только те данные, где последнее действие - contacts_show
contacts_show = final_df.query('last_event == "contacts_show"')
contacts_show
| index | user_id | dt | last_event | session_start | event_name | session_dt | session_id | cnt_sessions | scenarios | |
|---|---|---|---|---|---|---|---|---|---|---|
| 91 | 91 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-30 | contacts_show | 2019-10-30 07:50:45.948358 | search | 2019-10-30 | 6375 | 14 | (photos_show, search, contacts_show) |
| 92 | 92 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-30 | contacts_show | 2019-10-30 07:53:12.730053 | photos_show | 2019-10-30 | 6375 | 14 | (photos_show, search, contacts_show) |
| 93 | 93 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-30 | contacts_show | 2019-10-30 07:54:25.826815 | photos_show | 2019-10-30 | 6375 | 14 | (photos_show, search, contacts_show) |
| 94 | 94 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-30 | contacts_show | 2019-10-30 07:55:09.436282 | search | 2019-10-30 | 6375 | 14 | (photos_show, search, contacts_show) |
| 95 | 95 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-30 | contacts_show | 2019-10-30 07:57:00.426097 | photos_show | 2019-10-30 | 6375 | 14 | (photos_show, search, contacts_show) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 74151 | 74151 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show | 2019-10-29 15:19:08.638211 | tips_show | 2019-10-29 | 6374 | 20 | (tips_show, contacts_show) |
| 74152 | 74152 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show | 2019-10-29 15:19:10.766992 | tips_show | 2019-10-29 | 6374 | 20 | (tips_show, contacts_show) |
| 74153 | 74153 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show | 2019-10-29 16:12:15.417343 | tips_show | 2019-10-29 | 6374 | 20 | (tips_show, contacts_show) |
| 74154 | 74154 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show | 2019-10-29 16:12:17.466193 | tips_show | 2019-10-29 | 6374 | 20 | (tips_show, contacts_show) |
| 74155 | 74155 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show | 2019-10-29 16:13:00.681459 | contacts_show | 2019-10-29 | 6374 | 20 | (tips_show, contacts_show) |
5012 rows × 10 columns
(contacts_show
.groupby('scenarios')
.agg({'session_id': 'nunique'})
.sort_values(by='session_id', ascending=False)
).head(20)
| session_id | |
|---|---|
| scenarios | |
| (contacts_show,) | 116 |
| (tips_show, contacts_show) | 98 |
| (map, tips_show, contacts_show) | 58 |
| (photos_show, contacts_show) | 54 |
| (search, contacts_show) | 27 |
| (search, photos_show, contacts_show) | 20 |
| (tips_click, tips_show, contacts_show) | 15 |
| (search, tips_show, contacts_show) | 13 |
| (contacts_call, contacts_show) | 11 |
| (map, advert_open, tips_show, contacts_show) | 7 |
| (map, contacts_show) | 6 |
| (photos_show, search, contacts_show) | 6 |
| (tips_show, map, contacts_show) | 5 |
| (advert_open, contacts_show) | 4 |
| (contacts_call, photos_show, contacts_show) | 4 |
| (search, map, tips_show, contacts_show) | 4 |
| (search, favorites_add, photos_show, contacts_show) | 3 |
| (search, map, advert_open, tips_show, contacts_show) | 3 |
| (search, contacts_call, contacts_show) | 3 |
| (map, advert_open, contacts_show) | 3 |
Уберем события tips_show из датасета и посмотрим сценарии
sessions_2 = sessions.query('event_name != "tips_show"')
sessions_2
| session_start | event_name | user_id | dt | |
|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| 6 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 8 | 2019-10-07 00:01:49.732803 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| 9 | 2019-10-07 00:01:54.958298 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| ... | ... | ... | ... | ... |
| 74185 | 2019-11-03 23:49:19.075954 | tips_click | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 | 2019-11-03 |
| 74187 | 2019-11-03 23:50:05.753036 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 |
| 74188 | 2019-11-03 23:51:08.879296 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 |
| 74189 | 2019-11-03 23:52:01.835490 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 |
| 74194 | 2019-11-03 23:56:57.041825 | search | 20850c8f-4135-4059-b13b-198d3ac59902 | 2019-11-03 |
34142 rows × 4 columns
# создаем профиль пользователей с последним действием
profiles_last_2 = (
sessions_2.sort_values(by=['user_id', 'session_start'])
.groupby(['user_id', 'dt'])
.agg({'event_name': 'last'})
.rename(columns={'event_name': 'last_event', 'dt': 'last_dt'})
.reset_index()
)
profiles_last_2
| user_id | dt | last_event | |
|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | map |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | map |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | map |
| 3 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | photos_show |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | photos_show |
| ... | ... | ... | ... |
| 6127 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-28 | contacts_show |
| 6128 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-29 | contacts_show |
| 6129 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-10-30 | contacts_show |
| 6130 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-02 | contacts_show |
| 6131 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show |
6132 rows × 3 columns
# соединим таблицы, чтобы получить к каждому last_event - событие с event_name
result_raw_last_2 = profiles_last_2.merge(
sessions_2[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])
result_raw_last_2['session_dt'] = result_raw_last_2['session_start'].dt.date
result_raw_last_2
| index | user_id | dt | last_event | session_start | event_name | session_dt | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | map | 2019-10-09 18:33:55.577963 | map | 2019-10-09 |
| 1 | 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | map | 2019-10-09 18:35:28.260975 | map | 2019-10-09 |
| 2 | 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | map | 2019-10-21 19:53:38.767230 | map | 2019-10-21 |
| 3 | 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | map | 2019-10-21 19:56:49.417415 | map | 2019-10-21 |
| 4 | 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | map | 2019-10-22 11:18:14.635436 | map | 2019-10-22 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 34137 | 34137 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show | 2019-11-03 14:38:51.134084 | contacts_show | 2019-11-03 |
| 34138 | 34138 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show | 2019-11-03 14:41:24.780546 | contacts_show | 2019-11-03 |
| 34139 | 34139 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show | 2019-11-03 14:42:26.444553 | contacts_show | 2019-11-03 |
| 34140 | 34140 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show | 2019-11-03 15:48:05.420247 | contacts_show | 2019-11-03 |
| 34141 | 34141 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | contacts_show | 2019-11-03 15:51:57.899997 | contacts_show | 2019-11-03 |
34142 rows × 7 columns
# группируем датасет по session_dt, user_id, last_event
# тем самым получаем количество действий пользователя в день
# а индексы будут означать session_id каждой сессии, их переименуем в session_id
final_df_last_2 = result_raw_last_2.pivot_table(
index = ['session_dt', 'user_id', 'last_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})
final_df_last_2 = final_df_last_2.reset_index().rename(columns = {'index': 'session_id'})
final_df_last_2
| session_id | session_dt | user_id | last_event | cnt_sessions | |
|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 |
| 1 | 1 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 13 |
| 2 | 2 | 2019-10-07 | 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 | search | 3 |
| 3 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 |
| 4 | 4 | 2019-10-07 | 063c0b06-6d9d-4580-8d03-f1c8f19ebfa1 | photos_show | 5 |
| ... | ... | ... | ... | ... | ... |
| 6127 | 6127 | 2019-11-03 | f6f94ebe-e69a-4ae3-9fb0-312d52d35826 | photos_show | 3 |
| 6128 | 6128 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | contacts_show | 5 |
| 6129 | 6129 | 2019-11-03 | f8cfaf96-f241-4da7-ae8a-618aaf3a7fee | advert_open | 1 |
| 6130 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 |
| 6131 | 6131 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | contacts_show | 7 |
6132 rows × 5 columns
# далее присоединяем к прошлой таблице наш датасет, чтобы дальше можно было сгруппировать по event_name
# избавимся от повторяющихся событий в рамках сессии и оставляем последние действия
final_df_last_2 = final_df_last_2.merge(
result_raw_last_2, on = ['user_id', 'session_dt', 'last_event'], how = 'left'
).drop_duplicates(subset = ['session_id', 'event_name'], keep='last').reset_index(drop=True)
final_df_last_2
| session_id | session_dt | user_id | last_event | cnt_sessions | index | dt | session_start | event_name | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 | 200 | 2019-10-07 | 2019-10-07 22:01:43.074554 | search |
| 1 | 1 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 13 | 275 | 2019-10-07 | 2019-10-07 00:15:47.030323 | map |
| 2 | 1 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 13 | 277 | 2019-10-07 | 2019-10-07 00:18:37.324815 | advert_open |
| 3 | 2 | 2019-10-07 | 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 | search | 3 | 316 | 2019-10-07 | 2019-10-07 13:19:35.882141 | search |
| 4 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 650 | 2019-10-07 | 2019-10-07 10:43:49.168937 | search |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 10127 | 6129 | 2019-11-03 | f8cfaf96-f241-4da7-ae8a-618aaf3a7fee | advert_open | 1 | 33297 | 2019-11-03 | 2019-11-03 15:01:08.122940 | advert_open |
| 10128 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33337 | 2019-11-03 | 2019-11-03 11:38:41.424157 | search |
| 10129 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33338 | 2019-11-03 | 2019-11-03 11:39:32.218473 | contacts_show |
| 10130 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33339 | 2019-11-03 | 2019-11-03 11:39:43.578085 | contacts_call |
| 10131 | 6131 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | contacts_show | 7 | 34141 | 2019-11-03 | 2019-11-03 15:51:57.899997 | contacts_show |
10132 rows × 9 columns
# выведем все сценарии для каждой сессии:
# сгруппируем по session_id и добавим вывод сценариев с функцией лямбда и добавлением действий поочердно в кортеж
scenarios_2 = (final_df_last_2
.groupby('session_id')
.agg({'event_name':lambda x: tuple(x)})
.reset_index()
.rename(columns = {'event_name': 'scenarios'})
)
scenarios_2
| session_id | scenarios | |
|---|---|---|
| 0 | 0 | (search,) |
| 1 | 1 | (map, advert_open) |
| 2 | 2 | (search,) |
| 3 | 3 | (search, contacts_show) |
| 4 | 4 | (photos_show,) |
| ... | ... | ... |
| 6127 | 6127 | (photos_show,) |
| 6128 | 6128 | (search, contacts_show) |
| 6129 | 6129 | (advert_open,) |
| 6130 | 6130 | (search, contacts_show, contacts_call) |
| 6131 | 6131 | (contacts_show,) |
6132 rows × 2 columns
# посчитаем количество уникальных сценарий
scenarios_2['scenarios'].nunique()
282
282 уникальных сценария без событий tips_show.
Для решения основных вопросов исследования - нам нужны только те, в которых последнее действие - целевое.
# добавим к нашей таблице колонку scenarios
final_df_last_2 = final_df_last_2.merge(scenarios_2, on='session_id', how='left')
final_df_last_2
final_df_last_2
| session_id | session_dt | user_id | last_event | cnt_sessions | index | dt | session_start | event_name | scenarios | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2019-10-07 | 00e714c6-66b9-4091-bce0-8589bd65b77c | search | 2 | 200 | 2019-10-07 | 2019-10-07 22:01:43.074554 | search | (search,) |
| 1 | 1 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 13 | 275 | 2019-10-07 | 2019-10-07 00:15:47.030323 | map | (map, advert_open) |
| 2 | 1 | 2019-10-07 | 020292ab-89bc-4156-9acf-68bc2783f894 | advert_open | 13 | 277 | 2019-10-07 | 2019-10-07 00:18:37.324815 | advert_open | (map, advert_open) |
| 3 | 2 | 2019-10-07 | 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 | search | 3 | 316 | 2019-10-07 | 2019-10-07 13:19:35.882141 | search | (search,) |
| 4 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 650 | 2019-10-07 | 2019-10-07 10:43:49.168937 | search | (search, contacts_show) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 10127 | 6129 | 2019-11-03 | f8cfaf96-f241-4da7-ae8a-618aaf3a7fee | advert_open | 1 | 33297 | 2019-11-03 | 2019-11-03 15:01:08.122940 | advert_open | (advert_open,) |
| 10128 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33337 | 2019-11-03 | 2019-11-03 11:38:41.424157 | search | (search, contacts_show, contacts_call) |
| 10129 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33338 | 2019-11-03 | 2019-11-03 11:39:32.218473 | contacts_show | (search, contacts_show, contacts_call) |
| 10130 | 6130 | 2019-11-03 | f9c19253-73e7-4b9e-9630-45353a792248 | contacts_call | 3 | 33339 | 2019-11-03 | 2019-11-03 11:39:43.578085 | contacts_call | (search, contacts_show, contacts_call) |
| 10131 | 6131 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | contacts_show | 7 | 34141 | 2019-11-03 | 2019-11-03 15:51:57.899997 | contacts_show | (contacts_show,) |
10132 rows × 10 columns
# оставим в датасете только те данные, где последнее действие - contacts_show
contacts_show_2 = final_df_last_2.query('last_event == "contacts_show"')
contacts_show_2
| session_id | session_dt | user_id | last_event | cnt_sessions | index | dt | session_start | event_name | scenarios | |
|---|---|---|---|---|---|---|---|---|---|---|
| 4 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 650 | 2019-10-07 | 2019-10-07 10:43:49.168937 | search | (search, contacts_show) |
| 5 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 651 | 2019-10-07 | 2019-10-07 11:28:52.489988 | contacts_show | (search, contacts_show) |
| 24 | 16 | 2019-10-07 | 136b7b37-2bd4-4718-b14a-e38bc3d6d112 | contacts_show | 1 | 3251 | 2019-10-07 | 2019-10-07 21:38:02.944592 | contacts_show | (contacts_show,) |
| 36 | 24 | 2019-10-07 | 1a3361d1-2002-4389-a669-ecb06ea7a90a | contacts_show | 1 | 4257 | 2019-10-07 | 2019-10-07 10:19:49.080484 | contacts_show | (contacts_show,) |
| 47 | 29 | 2019-10-07 | 1fc68b5e-374b-4fc8-83cf-ac2307a8556c | contacts_show | 1 | 5027 | 2019-10-07 | 2019-10-07 20:32:14.100803 | contacts_show | (contacts_show,) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 10121 | 6125 | 2019-11-03 | ec70be94-3ea7-4ac2-90fb-da1a044d7e30 | contacts_show | 14 | 31848 | 2019-11-03 | 2019-11-03 20:12:29.063329 | search | (search, contacts_show) |
| 10122 | 6125 | 2019-11-03 | ec70be94-3ea7-4ac2-90fb-da1a044d7e30 | contacts_show | 14 | 31860 | 2019-11-03 | 2019-11-03 23:37:32.236349 | contacts_show | (search, contacts_show) |
| 10125 | 6128 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | contacts_show | 5 | 33124 | 2019-11-03 | 2019-11-03 15:58:59.729391 | search | (search, contacts_show) |
| 10126 | 6128 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | contacts_show | 5 | 33125 | 2019-11-03 | 2019-11-03 16:07:10.499238 | contacts_show | (search, contacts_show) |
| 10131 | 6131 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | contacts_show | 7 | 34141 | 2019-11-03 | 2019-11-03 15:51:57.899997 | contacts_show | (contacts_show,) |
1502 rows × 10 columns
(contacts_show_2
.groupby('scenarios')
.agg({'session_id': 'nunique'})
.sort_values(by='session_id', ascending=False)
).head(20)
| session_id | |
|---|---|
| scenarios | |
| (contacts_show,) | 402 |
| (map, contacts_show) | 137 |
| (search, contacts_show) | 73 |
| (photos_show, contacts_show) | 55 |
| (tips_click, contacts_show) | 31 |
| (search, photos_show, contacts_show) | 20 |
| (map, advert_open, contacts_show) | 13 |
| (contacts_call, contacts_show) | 11 |
| (advert_open, contacts_show) | 10 |
| (search, map, contacts_show) | 10 |
| (favorites_add, contacts_show) | 9 |
| (map, tips_click, contacts_show) | 7 |
| (map, search, advert_open, contacts_show) | 7 |
| (photos_show, search, contacts_show) | 6 |
| (search, map, advert_open, contacts_show) | 6 |
| (tips_click, map, contacts_show) | 5 |
| (map, favorites_add, contacts_show) | 4 |
| (map, search, contacts_show) | 4 |
| (contacts_call, photos_show, contacts_show) | 4 |
| (tips_click, search, contacts_show) | 3 |
Для дальнейшего анализа возьмём 4 самые популярные сценарии:
Сценарий (contacts_show,) рассматривать не будем, т.к. данный сценарий скорее является исключением, чем расскажет нам о поведении пользователей. Скорее всего данный сценарий возникает, когда пользователь проходит по ссылке на объявление и поэтому происходит сразу целевое действие.
Сценарий (tips_click, contacts_show) возникает только после события tips_show, который мы не рассматриваем для данного задания. Поэтому данный сценарий тоже не будем рассматривать, а лучше возмём следующий самый популярный сценарий с 3 событиями в сценарии - (search, photos_show, contacts_show).
contacts_show_2
| session_id | session_dt | user_id | last_event | cnt_sessions | index | dt | session_start | event_name | scenarios | |
|---|---|---|---|---|---|---|---|---|---|---|
| 4 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 650 | 2019-10-07 | 2019-10-07 10:43:49.168937 | search | (search, contacts_show) |
| 5 | 3 | 2019-10-07 | 057a4dc6-0698-4662-ba56-64f2bc6e0adc | contacts_show | 3 | 651 | 2019-10-07 | 2019-10-07 11:28:52.489988 | contacts_show | (search, contacts_show) |
| 24 | 16 | 2019-10-07 | 136b7b37-2bd4-4718-b14a-e38bc3d6d112 | contacts_show | 1 | 3251 | 2019-10-07 | 2019-10-07 21:38:02.944592 | contacts_show | (contacts_show,) |
| 36 | 24 | 2019-10-07 | 1a3361d1-2002-4389-a669-ecb06ea7a90a | contacts_show | 1 | 4257 | 2019-10-07 | 2019-10-07 10:19:49.080484 | contacts_show | (contacts_show,) |
| 47 | 29 | 2019-10-07 | 1fc68b5e-374b-4fc8-83cf-ac2307a8556c | contacts_show | 1 | 5027 | 2019-10-07 | 2019-10-07 20:32:14.100803 | contacts_show | (contacts_show,) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 10121 | 6125 | 2019-11-03 | ec70be94-3ea7-4ac2-90fb-da1a044d7e30 | contacts_show | 14 | 31848 | 2019-11-03 | 2019-11-03 20:12:29.063329 | search | (search, contacts_show) |
| 10122 | 6125 | 2019-11-03 | ec70be94-3ea7-4ac2-90fb-da1a044d7e30 | contacts_show | 14 | 31860 | 2019-11-03 | 2019-11-03 23:37:32.236349 | contacts_show | (search, contacts_show) |
| 10125 | 6128 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | contacts_show | 5 | 33124 | 2019-11-03 | 2019-11-03 15:58:59.729391 | search | (search, contacts_show) |
| 10126 | 6128 | 2019-11-03 | f73fb5e2-7b99-45a0-bffa-29ff320399c1 | contacts_show | 5 | 33125 | 2019-11-03 | 2019-11-03 16:07:10.499238 | contacts_show | (search, contacts_show) |
| 10131 | 6131 | 2019-11-03 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | contacts_show | 7 | 34141 | 2019-11-03 | 2019-11-03 15:51:57.899997 | contacts_show | (contacts_show,) |
1502 rows × 10 columns
# оставим в датасете только действия map | (map)
map = final_df_last_2.query('event_name == "map"')
# посчитаем их количество
print('Количество пользователей совершивших (search):', map['user_id'].nunique())
# добавим пользователей, которые совершили данное действие в лист
map_users_list = map['user_id'].unique().tolist()
# оставим в датасете все действия пользователей, которые совершили действие map
map_funnel = final_df_last_2.query('user_id in @map_users_list')
# теперь в прошлой таблице оставим только тех, кто совершил (map, contacts_show) | (map, contacts_show)
contacts_show = map_funnel.query('event_name == "contacts_show"')
# посчитаем их количество
print('Количество пользователей совершивших (search, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1456 Количество пользователей совершивших (search, contacts_show): 289
# построим воронку для сценария (tips_show, map, contacts_show)
fig = go.Figure(go.Funnel(
y = ['map', 'contacts_show'],
x = [map['user_id'].nunique(), contacts_show['user_id'].nunique()],
textposition = 'inside',
textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария search-contacts_show')
fig.show()
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['map',
'contacts_show'],
'cnt_users': [map['user_id'].nunique(),
contacts_show['user_id'].nunique()]})
funnel = funnel.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100
# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))
# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])
funnel
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | map | 1456 | 40.60 | NaN | 1.00 |
| 1 | contacts_show | 289 | 8.06 | 0.20 | 0.20 |
CR от открытия карты объявления к целевому действию - 20%.
RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.
# оставим в датасете только действия search | (search)
search = final_df_last_2.query('event_name == "search"')
# посчитаем их количество
print('Количество пользователей совершивших (search):', search['user_id'].nunique())
# добавим пользователей, которые совершили данное действие в лист
search_users_list = search['user_id'].unique().tolist()
# оставим в датасете все действия пользователей, которые совершили действие search
search_funnel = final_df_last_2.query('user_id in @search_users_list')
# теперь в прошлой таблице оставим только тех, кто совершил (search, contacts_show) | (search, contacts_show)
contacts_show = search_funnel.query('event_name == "contacts_show"')
# посчитаем их количество
print('Количество пользователей совершивших (search, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1666 Количество пользователей совершивших (search, contacts_show): 377
# построим воронку для сценария (tips_show, map, contacts_show)
fig = go.Figure(go.Funnel(
y = ['search', 'contacts_show'],
x = [search['user_id'].nunique(), contacts_show['user_id'].nunique()],
textposition = 'inside',
textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария search-contacts_show')
fig.show()
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['search',
'contacts_show'],
'cnt_users': [search['user_id'].nunique(),
contacts_show['user_id'].nunique()]})
funnel = funnel.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100
# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))
# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])
funnel
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | search | 1666 | 46.46 | NaN | 1.00 |
| 1 | contacts_show | 377 | 10.51 | 0.23 | 0.23 |
CR от поиска объявления к целевому действию - 23%.
RR для действия searсh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.
# оставим в датасете только действия photos_show | (photos_show)
photos_show = final_df_last_2.query('event_name == "photos_show"')
# посчитаем их количество
print('Количество пользователей совершивших (photos_show):', photos_show['user_id'].nunique())
# добавим пользователей, которые совершили данное действие в лист
photos_show_users_list = photos_show['user_id'].unique().tolist()
# оставим в датасете все действия пользователей, которые совершили действие photos_show
photos_show_funnel = final_df_last_2.query('user_id in @photos_show_users_list')
# теперь в прошлой таблице оставим только тех, кто совершил contacts_show | (photos_show, contacts_show)
contacts_show = photos_show_funnel.query('event_name == "contacts_show"')
# посчитаем их количество
print('Количество пользователей совершивших (photos_show, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (photos_show): 1095 Количество пользователей совершивших (photos_show, contacts_show): 339
# построим воронку для сценария (tips_show, contacts_show)
fig = go.Figure(go.Funnel(
y = ['photos_show', 'contacts_show'],
x = [photos_show['user_id'].nunique(), contacts_show['user_id'].nunique()],
textposition = 'inside',
textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария photos_show-contacts_show')
fig.show()
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['photos_show',
'contacts_show'],
'cnt_users': [photos_show['user_id'].nunique(),
contacts_show['user_id'].nunique()]})
funnel = funnel.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100
# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))
# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])
funnel
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | photos_show | 1095 | 30.54 | NaN | 1.00 |
| 1 | contacts_show | 339 | 9.45 | 0.31 | 0.31 |
CR от просмотревших фото объявления к целевому действию - 31%.
RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.
# т.к. мы уже провели расчеты по совершавшим search - сразу посчитаем их количество
print('Количество пользователей совершивших (search):', search['user_id'].nunique())
# оставим только тех, кто совершил photos_show, после search | (search, photos_show)
photos_show = search_funnel.query('event_name == "photos_show"')
# посчитаем их количество
print('Количество пользователей совершивших (search, photos_show):', photos_show['user_id'].nunique())
# добавим пользователей, которые совершили действие (search, photos_show) в лист
photos_show_users_list = photos_show['user_id'].unique().tolist()
# выведем все действия тех пользователей, которые совершили действие (search, photos_show)
photos_show_funnel = final_df_last_2.query(('user_id in @photos_show_users_list'))
# оставим только тех, кто совершил (search, photos_show, contacts_show) | (search, photos_show, contacts_show)
contacts_show = photos_show_funnel.query('event_name == "contacts_show"')
# посчитаем их количество
print('Количество пользователей совершивших (search, photos_show, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1666 Количество пользователей совершивших (search, photos_show): 647 Количество пользователей совершивших (search, photos_show, contacts_show): 192
# построим воронку для сценария (search, photos_show, contacts_show)
fig = go.Figure(go.Funnel(
y = ['search', 'photos_show', 'contacts_show'],
x = [search['user_id'].nunique(), photos_show['user_id'].nunique(), contacts_show['user_id'].nunique()],
textposition = 'auto',
textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария (search, photos_show, contacts_show)')
fig.show()
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['search',
'photos_show',
'contacts_show'],
'cnt_users': [search['user_id'].nunique(),
photos_show['user_id'].nunique(),
contacts_show['user_id'].nunique()]})
funnel = funnel.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100
# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))
# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])
funnel
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | search | 1666 | 46.46 | NaN | 1.00 |
| 1 | photos_show | 647 | 18.04 | 0.39 | 0.39 |
| 2 | contacts_show | 192 | 5.35 | 0.30 | 0.12 |
CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?
RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.
Один из верных способов, по моему мнению, это сделать геймификацию: Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд).
СЦЕНАРИЙ (tips_show, contacts_show)
CR от просмотревших рекомендованное объявление к целевому действию - 18,4%.
RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия contacts_show от общего количества пользователей составляет 12,02%.
СЦЕНАРИЙ (tips_show, map, contacts_show)
CR от tips_show к map составляет 48,3%, от map к целевому действию - 20,3%.
RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия map от общего количества пользователей составляет 31,49%. RR для действия contacts_show от общего количества пользователей составляет 6,41%.
Лишь 10% от tips_show дошли до целевого действия.
СЦЕНАРИЙ (tips_show, tips_click, contacts_show)
CR от tips_show к tips_click составляет 10,6%, от tips_click к целевому действию - 30,6%. Очень достойный показатель, это значит, что рекомендации очень хорошо настроены и пользователь, который прошёл по рекомендации, с вероятностью 30% добавит контакты продавца. Раз рекомендации работают хорошо, значит нужно, чтобы люди чаще переходили по ним.
RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия tips_click от общего количества пользователей составляет 6,92%. RR для действия contacts_show от общего количества пользователей составляет 2,12%.
Проблема в том, что люди не доверяют рекомендациям, либо они привыкли не обращать на них свое внимание. И именно из-за этой проблемы лишь 2,9% от tips_show доходят до целевого действия.
Самый верный вариант, по моему мнению, это сделать геймификацию: Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенное количество tips_click. Данный способ может сработать.
СЦЕНАРИЙ (map, contacts_show)
CR от открытия карты объявления к целевому действию - 20%.
RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.
СЦЕНАРИЙ (search, contacts_show)
CR от поиска объявления к целевому действию - 23%.
RR для действия searxh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.
СЦЕНАРИЙ (photos_show, contacts_show)
CR от просмотревших фото объявления к целевому действию - 31%.
RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.
СЦЕНАРИЙ (search, photos_show, contacts_show)
CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?
RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.
ВЫВОД
Для увеличения активности пользователей - можно сделать сделать геймификацию:\ Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд). Данный способ поможет поднять активность пользователей.
Также для увеличения активности пользователей можно расширить функционал приложения добавляя новые возможности: возможность выбора транспортной компанией в случае успешной продажи; возможность добавления (также их привлечение) компаний по продажам вещей, например, секондхенды или оффлайн магазины и т.д.
Для улучшения работы приложения не в количестве аудитории, а в качестве стоит подумать над новой рекомендательной системой или её улучшением. Довольно большое количество событий tips_show было показано пользователям, а конверсия от tips_show к tips_click составляет всего лишь 10,6%. Возможно пользователи не видят этих рекомендаций, может стоит увеличить их в размере или поставить в более видное место, может они просто не хотят переходить по ним (в последнем случае поможет геймификация). Стоит разобраться с этой проблемой.\ Сама рекомендательная система показывает 30% конверсию от tips_click к целевому действию, над этими цифрами тоже можно поработать улучшив саму рекомендательную систему.
Для ответа на вопрос - поделим датасет на:
df
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 2 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 3 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 4 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| ... | ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:46:47.068179 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74193 | 2019-11-03 23:46:58.914787 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74194 | 2019-11-03 23:47:01.232230 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74195 | 2019-11-03 23:47:47.475102 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74196 | 2019-11-03 23:47:50.087645 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 |
74197 rows × 5 columns
# разделим датасет на 2 части: на пользователей, которые смотрели и не смотрели контакты
all_users = df
print('Количество всех пользователей:', all_users['user_id'].nunique())
show_users = df.query('event_name == "contacts_show"')
print('Количество пользователей, которые смотрели контакты:', show_users['user_id'].nunique())
show_users_list = show_users['user_id'].unique().tolist()
show_users = df.query('user_id in @show_users_list')
dont_show_users = df.query('user_id not in @show_users_list')
print('Количество пользователей, которые не смотрели контакты:', dont_show_users['user_id'].nunique())
Количество всех пользователей: 4293 Количество пользователей, которые смотрели контакты: 981 Количество пользователей, которые не смотрели контакты: 3312
show_users
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 121 | 2019-10-07 00:02:07.374346 | tips_show | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | 2019-10-07 | yandex |
| 122 | 2019-10-07 00:05:16.003328 | contacts_show | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | 2019-10-07 | yandex |
| 123 | 2019-10-07 00:06:01.349291 | tips_show | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | 2019-10-07 | yandex |
| 124 | 2019-10-07 00:06:56.367054 | tips_show | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | 2019-10-07 | yandex |
| 125 | 2019-10-07 00:09:24.585200 | tips_show | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | 2019-10-07 | yandex |
| ... | ... | ... | ... | ... | ... |
| 74187 | 2019-11-03 23:46:31.298524 | contacts_show | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 | yandex |
| 74188 | 2019-11-03 23:48:47.344430 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 | yandex |
| 74189 | 2019-11-03 23:50:05.753036 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 | yandex |
| 74190 | 2019-11-03 23:51:08.879296 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 | yandex |
| 74191 | 2019-11-03 23:52:01.835490 | tips_click | c10055f0-0b47-477a-869e-d391b31fdf8f | 2019-11-03 | yandex |
27197 rows × 5 columns
Данные группы отличаются только тем, что в группе смотревших контакты присутствуют действия: contacts_show и contacts_call.
Их следует убрать, т.к. понятно, что если мы поделили группу, на совершивих и не совершивших целевое действие, то в первой группе будут данные действия, а во второй - их не будет. Поэтому чтобы разобраться в поведении пользователей эти действия стоит удалить из первой группы. contacts_call стоит удалить, т.к. данное действие является продолжением действия contacts_show.
show_users = show_users.query('event_name != "contacts_show" and event_name != "contacts_call"')
# посмотрим названия действий и количество таких событий среди смотревших контакты
show_users_events = show_users['event_name'].value_counts()
show_users_events.reset_index()
| event_name | count | |
|---|---|---|
| 0 | tips_show | 12768 |
| 1 | photos_show | 3828 |
| 2 | search | 2084 |
| 3 | advert_open | 1589 |
| 4 | map | 1101 |
| 5 | favorites_add | 424 |
| 6 | tips_click | 333 |
# посмотрим названия действий и количество таких событий среди не смотревших контакты
dont_show_users_events = dont_show_users['event_name'].value_counts()
dont_show_users_events.reset_index()
| event_name | count | |
|---|---|---|
| 0 | tips_show | 27287 |
| 1 | photos_show | 6184 |
| 2 | search | 4700 |
| 3 | advert_open | 4575 |
| 4 | map | 2780 |
| 5 | favorites_add | 993 |
| 6 | tips_click | 481 |
# построим круговые диаграммы по смотревшим контакты и не смотревших контакты
fig = make_subplots(rows=1, cols=2,
specs=[[{'type': 'domain'}, {'type': 'domain'}]],
subplot_titles=('Круговая диаграмма по целевому действию',
'Круговая диаграмма по не целевому действию'))
fig.add_trace(
go.Pie(labels=show_users_events.index,
values=show_users_events),
row=1, col=1
)
fig.add_trace(
go.Pie(labels=dont_show_users_events.index,
values=dont_show_users_events),
row=1, col=2
)
fig.update_layout(height=500, width=1000)
fig.show()
show_users_events
event_name tips_show 12768 photos_show 3828 search 2084 advert_open 1589 map 1101 favorites_add 424 tips_click 333 Name: count, dtype: int64
show_users_events.reset_index()
| event_name | count | |
|---|---|---|
| 0 | tips_show | 12768 |
| 1 | photos_show | 3828 |
| 2 | search | 2084 |
| 3 | advert_open | 1589 |
| 4 | map | 1101 |
| 5 | favorites_add | 424 |
| 6 | tips_click | 333 |
# строим столбчатую диаграмму по смотревшим контакты
fig = px.bar(show_users_events.reset_index(),
x='event_name',
y='count',
text='count',
color_discrete_sequence = px.colors.sequential.Plotly3)
# оформляем график
fig.update_layout(title='Диаграмма по целевому действию',
xaxis_title='Название совершеннного действия',
yaxis_title='Количество совершенных действий')
# выводим график
fig.show()
# строим столбчатую диаграмму по не смотревшим контакты
fig = px.bar(dont_show_users_events.reset_index(),
x='event_name',
y='count',
text='count',
color_discrete_sequence = px.colors.sequential.Plotly3)
# оформляем график
fig.update_layout(title='Диаграмма по не целевому действию',
xaxis_title='Название действия',
yaxis_title='Количество действий')
# выводим график
fig.show()
Очередность по количеству действий в данных группах точно такая же, т.е. топ по количеству действий в данных группах не отличается.
Можно отметить тот факт, что пользователи, которые просмотрели контакты - более активны. Больше действий в photos_show и map, чем у тех, кто не смотрел контакты. Но у достигших целевого действия - меньше открытий объявлений (advert_open).
# посмотрим названия действий и количество таких событий среди смотревших контакты
show_users_dt = show_users['dt'].value_counts()
show_users_dt
dt 2019-10-29 1128 2019-10-23 1052 2019-10-24 1051 2019-11-01 1023 2019-10-26 969 2019-10-28 950 2019-10-21 947 2019-10-25 906 2019-10-31 896 2019-10-27 868 2019-10-30 847 2019-11-03 835 2019-10-13 834 2019-10-18 829 2019-10-22 825 2019-10-15 773 2019-10-17 723 2019-10-10 705 2019-10-16 697 2019-10-14 679 2019-10-12 647 2019-11-02 645 2019-10-11 625 2019-10-19 580 2019-10-08 575 2019-10-09 529 2019-10-07 503 2019-10-20 486 Name: count, dtype: int64
dont_show_users_dt = dont_show_users['dt'].value_counts()
dont_show_users_dt
dt 2019-10-14 2123 2019-10-23 2048 2019-10-28 2005 2019-10-07 1974 2019-10-26 1958 2019-10-29 1894 2019-10-27 1876 2019-11-03 1874 2019-10-16 1829 2019-10-30 1815 2019-10-08 1782 2019-10-21 1771 2019-10-18 1733 2019-10-24 1727 2019-10-31 1709 2019-10-15 1705 2019-10-13 1690 2019-10-22 1669 2019-10-19 1641 2019-10-17 1595 2019-10-25 1505 2019-10-20 1445 2019-10-10 1413 2019-10-09 1389 2019-11-01 1319 2019-10-11 1311 2019-10-12 1132 2019-11-02 1068 Name: count, dtype: int64
# строим столбчатую диаграмму
fig = px.bar(show_users_dt.reset_index(),
x='dt',
y='count',
text='count',
color_discrete_sequence = px.colors.sequential.Plotly3)
# оформляем график
fig.update_layout(title='Диаграмма по целевому действию',
xaxis_title='Дата совершенных действий',
yaxis_title='Количество совершеннных действий')
# выводим график
fig.show()
# строим столбчатую диаграмму
fig = px.bar(dont_show_users_dt.reset_index(),
x='dt',
y='count',
text='count',
color_discrete_sequence = px.colors.sequential.Plotly3)
# оформляем график
fig.update_layout(title='Диаграмма по не целевому действию',
xaxis_title='Дата совершеннных действий',
yaxis_title='Количество совершенных действий')
# выводим график
fig.show()
По данным этих групп есть некая закономерность.
В диаграмме группы, достигших целевого действия - видно, что после сильной просадки (19.10) начинается увеличиваться активность, пик которой достигает (29.10), а дальше начинает падать.
А в диаграмме группы, не достигших целевого действия такой закономерности не видно. Данные за все время примерно находятся на одном уровне, хотя были 2 сильные просадки (12.10 и 02.11).
show_users_source = show_users['source'].value_counts()
show_users_source
source yandex 10238 google 6910 other 4979 Name: count, dtype: int64
dont_show_users_source = dont_show_users['source'].value_counts()
dont_show_users_source
source yandex 21787 other 13281 google 11932 Name: count, dtype: int64
# построим круговые диаграммы источников по смотревшим контакты и не смотревшим контакты
fig = make_subplots(rows=1, cols=2,
specs=[[{'type': 'domain'}, {'type': 'domain'}]],
subplot_titles=('Круговая диаграмма по целевому действию',
'Круговая диаграмма по не целевому действию'))
fig.add_trace(
go.Pie(labels=show_users_source.index,
values=show_users_source),
row=1, col=1
)
fig.add_trace(
go.Pie(labels=dont_show_users_source.index,
values=dont_show_users_source),
row=1, col=2
)
fig.update_layout(height=600, width=885)
fig.show()
Обе группы пользуются в равной степени yandex, но в группе достигших целевого действия на втором месте стоит google(31,2%), а в не достигших целевого действия на втором месте other (28,3%).
Подытожив, можно сказать, что разница между группами совсем маленькая, даже если где-то она и есть, то всего лишь на пару процентов. Нельзя с уверенностью сказать, что группа, которая достигает целевого действия - достигает именно из-за 'X' действия.
Но важно отметить, что ровно с третьей недели - увеличивается активность группы, совершивших целевое действие.
Для начала нужно разбить пользователей на 2 группы:
all_tips_show_users = df.query('event_name == "tips_show"')
print('Количество пользователей, которым показали рекомендацию:', all_tips_show_users['user_id'].nunique())
all_tips_show_users_list = all_tips_show_users['user_id'].unique().tolist()
all_tips_show_users = df.query('user_id in @all_tips_show_users_list')
tips_click_users = all_tips_show_users.query('event_name == "tips_click"')
print('Количество пользователей, которые кликнули по рекомендации:', tips_click_users['user_id'].nunique())
tips_click_users_list = tips_click_users['user_id'].unique().tolist()
tips_click_users = df.query('user_id in @tips_click_users_list')
dont_tips_click_users = all_tips_show_users.query('user_id not in @tips_click_users_list')
print('Количество пользователей, которые увидели рекомендацию, но не кликнули:',
dont_tips_click_users['user_id'].nunique())
Количество пользователей, которым показали рекомендацию: 2801 Количество пользователей, которые кликнули по рекомендации: 297 Количество пользователей, которые увидели рекомендацию, но не кликнули: 2504
Пользователи разбиты на группы. Далее нужно посчитать конверсию каждой группы в просмотр контактов.
Для этого найдём тех, пользователей, которые:
click_in_contacts_show = tips_click_users.query('event_name == "contacts_show"')
print('Количество пользователей, которые кликнули по рекомендации и поcмотрели контакты:',
click_in_contacts_show['user_id'].nunique())
print('Конверсия в просмотр контактов данной группы:',
round((click_in_contacts_show['user_id'].nunique() / tips_click_users['user_id'].nunique()) * 100, 1), '%')
print()
dont_click_in_contacts_show = dont_tips_click_users.query('event_name == "contacts_show"')
print('Количество пользователей, которые не кликнули по рекомендации, сами нашли объявление и посмотрели контакты:',
dont_click_in_contacts_show['user_id'].nunique())
print('Конверсия в просмотр контактов данной группы:',
round((dont_click_in_contacts_show['user_id'].nunique() / dont_tips_click_users['user_id'].nunique()) * 100, 1), '%')
Количество пользователей, которые кликнули по рекомендации и поcмотрели контакты: 91 Конверсия в просмотр контактов данной группы: 30.6 % Количество пользователей, которые не кликнули по рекомендации, сами нашли объявление и посмотрели контакты: 425 Конверсия в просмотр контактов данной группы: 17.0 %
Для наглядности сделаем таблицы по двум группам и их метрикам.
conversion_tips_click = pd.DataFrame({'event_name': ['tips_show',
'tips_click',
'contacts_show'],
'cnt_users': [all_tips_show_users['user_id'].nunique(),
tips_click_users['user_id'].nunique(),
click_in_contacts_show['user_id'].nunique()]})
conversion_tips_click = conversion_tips_click.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
conversion_tips_click['ratio, %'] = (conversion_tips_click['cnt_users'] / all_tips_show_users['user_id'].nunique()) * 100
# посчитаем конверсию пользователей от события к событию
conversion_tips_click['conversion, %'] = (conversion_tips_click['cnt_users'] / conversion_tips_click['cnt_users'].shift(1)) * 100
# посчитаем долю пользователей, которые доходят до ЦС
conversion_tips_click['funnel, %'] = (conversion_tips_click['cnt_users'] / conversion_tips_click['cnt_users'].values[0]) * 100
conversion_tips_click
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | tips_show | 2801 | 100.00 | NaN | 100.00 |
| 1 | tips_click | 297 | 10.60 | 10.60 | 10.60 |
| 2 | contacts_show | 91 | 3.25 | 30.64 | 3.25 |
conversion_dont_tips_click = pd.DataFrame({'event_name': ['tips_show',
'dont_tips_click',
'contacts_show'],
'cnt_users': [all_tips_show_users['user_id'].nunique(),
dont_tips_click_users['user_id'].nunique(),
dont_click_in_contacts_show['user_id'].nunique()]})
conversion_dont_tips_click = conversion_dont_tips_click.sort_values(by='cnt_users', ascending=False)
# посчитаем долю пользователей, которые совершили действия по данному событию
conversion_dont_tips_click['ratio, %'] = (conversion_dont_tips_click['cnt_users'] / all_tips_show_users['user_id'].nunique()) * 100
# посчитаем конверсию пользователей от события к событию
conversion_dont_tips_click['conversion, %'] = (conversion_dont_tips_click['cnt_users'] / conversion_dont_tips_click['cnt_users'].shift(1)) * 100
# посчитаем долю пользователей, которые доходят до ЦС
conversion_dont_tips_click['funnel, %'] = (conversion_dont_tips_click['cnt_users'] / conversion_dont_tips_click['cnt_users'].values[0]) * 100
conversion_dont_tips_click
| event_name | cnt_users | ratio, % | conversion, % | funnel, % | |
|---|---|---|---|---|---|
| 0 | tips_show | 2801 | 100.00 | NaN | 100.00 |
| 1 | dont_tips_click | 2504 | 89.40 | 89.40 | 89.40 |
| 2 | contacts_show | 425 | 15.17 | 16.97 | 15.17 |
Конверсии посчитаны, можно приступать к проверке гипотез.
Напишем нулевую и альтернативную гипотезы.
H0: Различий в конверсии в целевое действие между группами (group_1 - tips_show, tips_click и group_2 - tips_show) нет;
H1: Есть различия в конверсии в целевое действие между группами (group_1 - tips_show, tips_click и group_2 - tips_show).
# создадим функцию для проверки гипотезы о равенстве долей
# alpha = 0.05 # критический уровень статистической значимости
def z_test(alpha):
#group_1 = logs[logs['group'] == group1]
#group_2 = logs[logs['group'] == group2]
display('group_1:', tips_click_users)
display('group_2:', dont_tips_click_users)
trials = np.array([tips_click_users['user_id'].nunique(),
dont_tips_click_users['user_id'].nunique()])
print('trials:', trials)
successes = np.array([click_in_contacts_show['user_id'].nunique(),
dont_click_in_contacts_show['user_id'].nunique()])
print('successes:', successes)
print()
# пропорция успехов в первой группе:
p1 = successes[0] / trials[0]
print('Конверсия в первой группе равна:', '{0:.2%}'.format(p1))
# пропорция успехов во второй группе:
p2 = successes[1] / trials[1]
print('Конверсия во второй группе равна:', '{0:.2%}'.format(p2))
print()
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
print('p_combined:', p_combined)
# разница пропорций в датасетах
difference = p1 - p2
print('difference:', difference)
assert((p_combined * (1 - p_combined) * (1 / trials[0] + 1 / trials[1])) > 0)
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1 / trials[0] + 1 / trials[1]))
print('z_value:', z_value)
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print()
print('p-значение: ', p_value)
print()
if p_value < alpha:
print('p-value < alpha, поэтому нужно отвергнуть нулевую гипотезу.')
print('Есть основания говорить, что между долями есть существенные различия.')
else:
print('p-value > alpha, поэтому нельзя отвергнуть нулевую гипотезу.')
print('Есть основания говорить, что между долями нет существенных различий.')
z_test(0.05)
'group_1:'
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 28 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 | yandex |
| 29 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 | yandex |
| 30 | 2019-10-07 00:01:19.993624 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 | yandex |
| 31 | 2019-10-07 00:01:49.732803 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 | yandex |
| 32 | 2019-10-07 00:02:06.225301 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 | yandex |
| ... | ... | ... | ... | ... | ... |
| 73424 | 2019-11-03 16:51:57.923473 | tips_show | 3c14b914-b869-47d3-876b-0e83f9016e28 | 2019-11-03 | yandex |
| 73425 | 2019-11-03 16:54:31.870127 | tips_show | 3c14b914-b869-47d3-876b-0e83f9016e28 | 2019-11-03 | yandex |
| 73426 | 2019-11-03 16:56:01.603678 | tips_click | 3c14b914-b869-47d3-876b-0e83f9016e28 | 2019-11-03 | yandex |
| 73427 | 2019-11-03 16:56:08.706679 | tips_show | 3c14b914-b869-47d3-876b-0e83f9016e28 | 2019-11-03 | yandex |
| 73428 | 2019-11-03 16:57:04.230274 | tips_show | 3c14b914-b869-47d3-876b-0e83f9016e28 | 2019-11-03 | yandex |
12262 rows × 5 columns
'group_2:'
| session_start | event_name | user_id | dt | source | |
|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 2 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 3 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| 4 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other |
| ... | ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:46:47.068179 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74193 | 2019-11-03 23:46:58.914787 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74194 | 2019-11-03 23:47:01.232230 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74195 | 2019-11-03 23:47:47.475102 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | |
| 74196 | 2019-11-03 23:47:50.087645 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 |
44065 rows × 5 columns
trials: [ 297 2504] successes: [ 91 425] Конверсия в первой группе равна: 30.64% Конверсия во второй группе равна: 16.97% p_combined: 0.18421992145662264 difference: 0.13666887189251406 z_value: 5.744518044318512 p-значение: 9.218316554537864e-09 p-value < alpha, поэтому нужно отвергнуть нулевую гипотезу. Есть основания говорить, что между долями есть существенные различия.
Мы провели исследование о равенстве долей двух групп пользователей. Пользователей разбили на 2 группы:
В тесте между долями были существенные различия, поэтому пришлось отвергнуть нулевую гипотезу. Конверсия в первой группе равна 30,64%, а во второй группе 16,97%. Это говорит о том, что конверсии пользователей в 2х группах - существенно различаются.
Для выполнения данной задачи нужно добавить в датасет новую колонку со временем сессии.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 session_start 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object 3 dt 74197 non-null object 4 source 74197 non-null object dtypes: datetime64[ns](1), object(4) memory usage: 2.8+ MB
df = df.merge(
final_df[['session_start', 'event_name', 'user_id', 'session_id']],
on=['session_start', 'event_name', 'user_id'],
how='left'
)
df
| session_start | event_name | user_id | dt | source | session_id | |
|---|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other | 2 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other | 2 |
| 2 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other | 2 |
| 3 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other | 2 |
| 4 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 | other | 2 |
| ... | ... | ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 23:46:47.068179 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | 7778 | |
| 74193 | 2019-11-03 23:46:58.914787 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | 7778 | |
| 74194 | 2019-11-03 23:47:01.232230 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | 7778 | |
| 74195 | 2019-11-03 23:47:47.475102 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | 7778 | |
| 74196 | 2019-11-03 23:47:50.087645 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 | 7778 |
74197 rows × 6 columns
# для удобства отсортируем датасет по user_id, тогда события каждого пользователя будут идти последовательно
df = df.sort_values(['user_id', 'session_start'])
# в колонке diff для каждого события отдельного пользователя посчитаем разницу между временем посещения страницы
# и временем посещения предыдущей страницы
df['diff'] = df.groupby('user_id')['session_start'].diff(1)
# вычислим время, проведенное на странице, руководствуясь временем посещения следующей страницы
# для этого сначала считаем разницу между предыдущей и следующей страницей внутри сессии
df['time_on_page'] = df.groupby(['session_id'])['session_start'].diff(1)
# сдвинем значение столбца time_on_page на одну строку вверх внутри отдельно взятой сессии
df['time_on_page'] = df.groupby(['session_id'])['time_on_page'].shift(-1)
# преобразуем time_on_page в секунды
df['time_on_page'] = df['time_on_page'] / np.timedelta64(1, 's')
df
| session_start | event_name | user_id | dt | source | session_id | diff | time_on_page | |
|---|---|---|---|---|---|---|---|---|
| 2171 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | other | 0 | NaT | 45.06 |
| 2172 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | other | 0 | 0 days 00:00:45.063550 | 34.67 |
| 2173 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | other | 0 | 0 days 00:00:34.669580 | 135.01 |
| 2174 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | other | 0 | 0 days 00:02:15.012972 | 130.18 |
| 2175 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | other | 0 | 0 days 00:02:10.182041 | 12.29 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 19048 | 2019-11-03 15:51:23.959572 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7816 | 0 days 00:00:27.886483 | 33.94 | |
| 19049 | 2019-11-03 15:51:57.899997 | contacts_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7816 | 0 days 00:00:33.940425 | 943.03 | |
| 19050 | 2019-11-03 16:07:40.932077 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7816 | 0 days 00:15:43.032080 | 37.27 | |
| 19051 | 2019-11-03 16:08:18.202734 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7816 | 0 days 00:00:37.270657 | 7.19 | |
| 19052 | 2019-11-03 16:08:25.388712 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7816 | 0 days 00:00:07.185978 | NaN |
74197 rows × 8 columns
df_1 = df.groupby(['user_id', 'session_id']).agg({'time_on_page': 'sum'}).reset_index()
df_1
| user_id | session_id | time_on_page | |
|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 0 | 595.73 |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | 507.39 |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | 899.27 |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3843 | 758.17 |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2972 | 1520.79 |
| ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 6374 | 8052.82 |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 6687 | 40562.52 |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7291 | 22.31 |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | 65641.52 |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | 5729.43 |
7817 rows × 3 columns
df_2 = df.groupby(['user_id', 'session_id', 'event_name']).agg({'time_on_page': 'median'}).reset_index()
df_2
| user_id | session_id | event_name | time_on_page | |
|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 0 | tips_show | 54.82 |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | map | 196.58 |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | tips_show | 114.23 |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | map | 48.97 |
| 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | tips_show | 46.39 |
| ... | ... | ... | ... | ... |
| 14769 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7291 | tips_show | 22.31 |
| 14770 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | contacts_show | 2106.21 |
| 14771 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | tips_show | 568.45 |
| 14772 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | contacts_show | 137.89 |
| 14773 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | tips_show | 33.94 |
14774 rows × 4 columns
df_2['show_is_true'] = df_2['event_name'] == 'contacts_show'
df_2
| user_id | session_id | event_name | time_on_page | show_is_true | |
|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 0 | tips_show | 54.82 | False |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | map | 196.58 | False |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | tips_show | 114.23 | False |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | map | 48.97 | False |
| 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | tips_show | 46.39 | False |
| ... | ... | ... | ... | ... | ... |
| 14769 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7291 | tips_show | 22.31 | False |
| 14770 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | contacts_show | 2106.21 | True |
| 14771 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | tips_show | 568.45 | False |
| 14772 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | contacts_show | 137.89 | True |
| 14773 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | tips_show | 33.94 | False |
14774 rows × 5 columns
За длительность сессии посчитал сумму времени событий в одной сессии, т.е. сколько времени пользователь был активен внутри одной сессии. Кажется данный метод подсчета показывает более точное время затраченное пользователем внутри одной сессии.
Проверим пропуски в колонке time on page в первом датасете:
df_1['isnull'] = df_1['time_on_page'].isnull()
df_1[df_1['isnull'] == True].count()
user_id 0 session_id 0 time_on_page 0 isnull 0 dtype: int64
Пропусков нет.
Далее посчитаем медианные длительности сессий для двух групп:
df_1
| user_id | session_id | time_on_page | isnull | |
|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 0 | 595.73 | False |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | 507.39 | False |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | 899.27 | False |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3843 | 758.17 | False |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2972 | 1520.79 | False |
| ... | ... | ... | ... | ... |
| 7812 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 6374 | 8052.82 | False |
| 7813 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 6687 | 40562.52 | False |
| 7814 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7291 | 22.31 | False |
| 7815 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | 65641.52 | False |
| 7816 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | 5729.43 | False |
7817 rows × 4 columns
df_2
| user_id | session_id | event_name | time_on_page | show_is_true | |
|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 0 | tips_show | 54.82 | False |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | map | 196.58 | False |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 408 | tips_show | 114.23 | False |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | map | 48.97 | False |
| 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3517 | tips_show | 46.39 | False |
| ... | ... | ... | ... | ... | ... |
| 14769 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7291 | tips_show | 22.31 | False |
| 14770 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | contacts_show | 2106.21 | True |
| 14771 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7531 | tips_show | 568.45 | False |
| 14772 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | contacts_show | 137.89 | True |
| 14773 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 7816 | tips_show | 33.94 | False |
14774 rows × 5 columns
all_sessions = df_1
print('Количество всех сессий:', all_sessions['session_id'].nunique())
show_sessions = df_2.query('event_name == "contacts_show"')
print('Количество сессий, в которых смотрели контакты:', show_sessions['session_id'].nunique())
show_sessions_list = show_sessions['session_id'].unique().tolist()
show_sessions = df_1.query('session_id in @show_sessions_list')
dont_show_sessions = df_1.query('session_id not in @show_sessions_list')
print('Количество сессий, в которых не смотрели контакты:', dont_show_sessions['session_id'].nunique())
Количество всех сессий: 7817 Количество сессий, в которых смотрели контакты: 1444 Количество сессий, в которых не смотрели контакты: 6373
Теперь можно приступать к проверке гипотез.
Напишем нулевую и альтернативную гипотезы.
H0: Нет различий в медианной длительности сессии между группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия (event_name != contacts_show));
H1: Есть различия в медианной длительности сессии между группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия(event_name != contacts_show)).
Для сравнения медианной длительности используем непараметрический тест Уилкоксона-Манна-Уитни.
show_sessions = show_sessions['time_on_page']
dont_show_sessions = dont_show_sessions['time_on_page']
alpha = 0.05
results = st.mannwhitneyu(show_sessions, dont_show_sessions)
print('p-значение:', results.pvalue)
print('Медианная длительность сессии в группе достигших целевого действия:', show_sessions.median())
print('Медианная длительность сессии в группе не достигших целевого действия:', dont_show_sessions.median())
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу: разница статистически значима')
else:
print('Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя')
p-значение: 1.4416460813065889e-74 Медианная длительность сессии в группе достигших целевого действия: 1992.762575 Медианная длительность сессии в группе не достигших целевого действия: 599.80295 Отвергаем нулевую гипотезу: разница статистически значима
Мы провели исследование о равенстве медианной длительности сессии между двумя группами пользователей:
Медианная длительность сессий в группе достигших целевого действия равна 1992 секундам, а во второй группе не достигших целевого действия - 599 секунд. Медианная длительность пользователей в 2х группах различаются существенно, нулевую гипотезу нужно отвергнуть.
Это говорит о том, что пользователи достигающие целевых действий пользуются приложением в 3.3 раза дольше, а также совершают в 2 раза больше действий чем пользователи нецелевой группы.
Для достижения целей данного исследования мной были соверешены следующие действия:
Основные вопросы исследования:
5.1. Анализ связь целевого события — просмотра контактов — и других действий пользователей.
Построить воронки по основным сценариям в разрезе уникальных пользователей.
5.2. Оценка действий которые совершают те пользователи, которые просматривают контакты.
5.2.1 Рассчет относительной частоты событий в разрезе двух групп пользователей:
Проверка статистических гипотез
С помощью данного исследования я стремился дать всестороннний анализ пользователей мобильного приложения "Ненужные вещи" для дальнейшего развития и планирования расходов компании.
Cамыми популярными оказались сценарии:
Сценарий (map, contacts_show):
CR от открытия карты объявления к целевому действию - 20%.
RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.
Сценарий (search, contacts_show):
CR от поиска объявления к целевому действию - 23%.
RR для действия searсh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.
Сценарий (photos_show, contacts_show):
CR от просмотревших фото объявления к целевому действию - 31%.
RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.
Сценарий (search, photos_show, contacts_show):
CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?
RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.
Также хочется отдельно отметить следующий важный сценарий для олицетворения рекомендательной системы в приложении:
Сценарий (tips_show, tips_click, contacts_show):
Conversion от tips_show к tips_click составляет 10,6%, от tips_click к целевому действию - 30,6%. Это значит, что пользователь, который прошёл по рекомендации - с вероятностью 30% добавит контакты продавца. По самой рекомендательной системе есть ещё куда расти, но проблема чуть в другом: люди не пользуются рекомендациями. И именно из-за этой проблемы лишь 2,9% от tips_show доходят до целевого действия.
Retention для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. Retention для действия tips_click от общего количества пользователей составляет 6,92%. Retention для действия contacts_show от общего количества пользователей составляет 2,12%.
Разница в совершенных действиях и источниках между группами незначительная. Нельзя с уверенностью сказать, что группа, которая достигает целевого действия - достигает именно из-за 'X' действия. Но пользователи, которые просмотрели контакты - более активны. Больше действий в photos_show и map, чем у тех, кто не смотрел контакты. Но у достигших целевого действия - меньше открытий объявлений (advert_open).
Важно ещё также отметить, что ровно с третьей недели - увеличивается активность группы, совершивших целевое действие.
В данном анализе была проверена гипотеза о различии конверсии в просмотры контактов. Для этого пользователей разбили на 2 группы:
В тесте между долями были существенные различия, поэтому пришлось отвергнуть нулевую гипотезу. Конверсия в первой группе равна 30,64%, а во второй группе 16,97%. Это говорит о том, что конверсии пользователей в 2х группах - различаются существенно.
Также мы провели исследование о равенстве медианной длительности сессии между двумя группами пользователей:
Медианная длительность сессий в группе достигших целевого действия равна 1992 секундам, а во второй группе не достигших целевого действия - 599 секунд. Медианная длительность пользователей в 2х группах различаются существенно, нулевую гипотезу пришлось отвергнуть.
Это говорит о том, что пользователи достигающие целевых действий пользуются приложением в 3.3 раза дольше и совершают больше действий чем пользователи нецелевой группы. Это может работать и в обратную сторону: чем более активным является пользователь (по длительности сессий и количеству действий), тем больше вероятность, что он достигнет целевого действия. Таким образом, одним из рекомендаций будет увеличение активности пользователей, для повышения вероятности достижения целевого действия.
Мои рекомендации:
Для увеличения активности пользователей - можно сделать сделать геймификацию:\ Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд). Данный способ поможет поднять активность пользователей.
Также для увеличения активности пользователей можно расширить функционал приложения добавляя новые возможности: возможность выбора транспортной компанией в случае успешной продажи; возможность добавления (также их привлечение) компаний по продажам вещей, например, секондхенды или оффлайн магазины и т.д.
Для улучшения работы приложения не в количестве аудитории, а в качестве стоит подумать над новой рекомендательной системой или её улучшением. Довольно большое количество событий tips_show было показано пользователям, а конверсия от tips_show к tips_click составляет всего лишь 10,6%. Возможно пользователи не видят этих рекомендаций, может стоит увеличить их в размере или поставить в более видное место, может они просто не хотят переходить по ним (в последнем случае поможет геймификация). Стоит разобраться с этой проблемой.\ Сама рекомендательная система показывает 30% конверсию от tips_click к целевому действию, над этими цифрами тоже можно поработать улучшив саму рекомендательную систему.
Но ещё также нужно обеспечивать постоянный поток новых пользователей с помощью поиска новых каналов привлечения (реклама в разных соц сетях, блогеров, может быть ТВ или шоу).
Для более подробной аналитики не помешало бы иметь информацию в датасете из каких каналов были привлечены новые пользователи, чтобы можно было отслеживать с каких каналов лучшее удержание пользователей, стоимость привлечения одного пользователя, расчеты LTV, ROI, CAC и в целом для прогнозирования маркетинговых расходов на будущее.